// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
<!--
  InfiniteScrollSelect.vue

  A reusable select component that supports:
  - Infinite scrolling with paginated API
  - Dynamic search filtering. Needs minimum
  - Deduplicated option loading
  - Auto-fetching of preselected value if not present in the initial result

  Usage Example:

  <infinite-scroll-select
    v-model:value="form.account"
    api="listAccounts"
    :apiParams="accountsApiParams"
    resourceType="account"
    optionValueKey="name"
    optionLabelKey="name"
    @change-option-value="handleAccountNameChange" />

  Props:
  - api (String, required): API command name (e.g., 'listAccounts')
  - apiParams (Object, optional): Additional parameters passed to the API
  - resourceType (String, required): The key in the API response containing the resource array (e.g., 'account')
  - optionValueKey (String, optional): Property to use as the value for options (e.g., 'name'). Default is 'id'
  - optionLabelKey (String, optional): Property to use as the label for options (e.g., 'name'). Default is 'name'
  - defaultOption (Object, optional): Preselected object to include initially
  - showIcon (Boolean, optional): Whether to show icon for the options. Default is true
  - defaultIcon (String, optional): Icon to be shown when there is no resource icon for the option. Default is 'cloud-outlined'
  - autoSelectFirstOption (Boolean, optional): Whether to automatically select the first option when options are loaded. Default is false

  Events:
  - @change-option-value (Function): Emits the selected option value(s) when value(s) changes. Do not use @change as it will give warnings and may not work
  - @change-option (Function): Emits the selected option object when value changes. Works only when mode is not multiple

  Features:
  - Debounced remote filtering
  - Custom dropdown footer/header (e.g., clear search button)
  - Handles preselection and fetches missing option automatically
-->
<template>
  <a-select
    :filter-option="false"
    :loading="loading"
    show-search
    placeholder="Select"
    @search="onSearchTimed"
    @popupScroll="onScroll"
    @change="onChange"
  >
    <template #dropdownRender="{ menuNode: menu }">
      <v-nodes :vnodes="menu" />
      <div v-if="!!searchQuery">
        <a-divider style="margin: 4px 0" />
        <div class="select-list-footer">
          <span>{{ formattedSearchFooterMessage }}</span>
          <close-outlined
            @mousedown="e => e.preventDefault()"
            @click="onSearch()" />
        </div>
      </div>
    </template>
    <a-select-option v-for="option in options" :key="option.id" :value="option[optionValueKey]">
      <span>
        <span v-if="showIcon && option.showicon !== false">
          <resource-icon v-if="option.icon && option.icon.base64image" :image="option.icon.base64image" size="1x" style="margin-right: 5px"/>
          <render-icon v-else :icon="defaultIcon" style="margin-right: 5px" />
        </span>
        <span>{{ optionLabelFn ? optionLabelFn(option) : option[optionLabelKey] }}</span>
      </span>
    </a-select-option>
  </a-select>
</template>

<script>
import { callAPI } from '@/api/index'
import ResourceIcon from '@/components/view/ResourceIcon'

export default {
  name: 'InfiniteScrollSelect',
  components: {
    ResourceIcon,
    VNodes: (_, { attrs }) => {
      return attrs.vnodes
    }
  },
  props: {
    api: {
      type: String,
      required: true
    },
    apiParams: {
      type: Object,
      required: null
    },
    resourceType: {
      type: String,
      required: true
    },
    optionValueKey: {
      type: String,
      default: 'id'
    },
    optionLabelKey: {
      type: String,
      default: 'name'
    },
    optionLabelFn: {
      type: Function,
      default: null
    },
    defaultOption: {
      type: Object,
      default: null
    },
    showIcon: {
      type: Boolean,
      default: true
    },
    defaultIcon: {
      type: String,
      default: 'cloud-outlined'
    },
    pageSize: {
      type: Number,
      default: null
    },
    autoSelectFirstOption: {
      type: Boolean,
      default: false
    }
  },
  data () {
    return {
      options: [],
      page: 1,
      totalCount: null,
      loading: false,
      searchQuery: '',
      searchTimer: null,
      scrollHandlerAttached: false,
      preselectedOptionValue: null,
      successiveFetches: 0,
      canSelectFirstOption: false
    }
  },
  created () {
    this.addDefaultOptionIfNeeded()
  },
  mounted () {
    this.preselectedOptionValue = this.$attrs.value
    this.fetchItems()
  },
  computed: {
    maxSuccessiveFetches () {
      return 10
    },
    computedPageSize () {
      return this.pageSize || this.$store.getters.defaultListViewPageSize
    },
    formattedSearchFooterMessage () {
      return `${this.$t('label.showing.results.for').replace('%x', this.searchQuery)}`
    }
  },
  watch: {
    apiParams () {
      this.onSearch()
    }
  },
  emits: ['change-option-value', 'change-option'],
  methods: {
    async fetchItems () {
      if (this.successiveFetches === 0 && this.loading) return
      this.loading = true
      const params = {
        page: this.page,
        pagesize: this.computedPageSize
      }
      if (this.searchQuery && this.searchQuery.length > 0) {
        params.keyword = this.searchQuery
      }
      if (this.apiParams) {
        Object.assign(params, this.apiParams)
      }
      if (this.showIcon) {
        params.showicon = true
      }
      callAPI(this.api, params).then(json => {
        const response = json[this.api.toLowerCase() + 'response'] || {}
        if (this.totalCount === null) {
          this.totalCount = response.count || 0
        }
        const newOpts = response[this.resourceType] || []
        const existingOptions = new Set(this.options.map(o => o[this.optionValueKey]))
        newOpts.forEach(opt => {
          if (!existingOptions.has(opt[this.optionValueKey])) {
            this.options.push(opt)
          }
        })
        this.page++
        this.checkAndFetchPreselectedOption()
      }).catch(error => {
        this.$notifyError(error)
      }).finally(() => {
        this.canSelectFirstOption = true
        if (this.successiveFetches === 0) {
          this.loading = false
        }
      })
    },
    checkAndFetchPreselectedOption () {
      if (!this.preselectedOptionValue ||
        (Array.isArray(this.preselectedOptionValue) && this.preselectedOptionValue.length === 0) ||
        this.successiveFetches >= this.maxSuccessiveFetches) {
        this.resetPreselectedOptionValue()
        if (!this.canSelectFirstOption && this.autoSelectFirstOption && this.options.length > 0) {
          this.$nextTick(() => {
            this.preselectedOptionValue = this.options[0][this.optionValueKey]
            this.onChange(this.preselectedOptionValue)
          })
        }
        return
      }
      const matchValue = Array.isArray(this.preselectedOptionValue) ? this.preselectedOptionValue[0] : this.preselectedOptionValue
      const match = this.options.find(entry => entry[this.optionValueKey] === matchValue)
      if (!match) {
        this.successiveFetches++
        if (this.options.length < this.totalCount) {
          this.fetchItems()
        } else {
          this.resetPreselectedOptionValue()
        }
        return
      }
      if (Array.isArray(this.preselectedOptionValue) && this.preselectedOptionValue.length > 1) {
        this.preselectedOptionValue = this.preselectedOptionValue.filter(o => o !== match)
      } else {
        this.resetPreselectedOptionValue()
      }
    },
    addDefaultOptionIfNeeded () {
      if (this.defaultOption) {
        this.canSelectFirstOption = true
        this.options.push(this.defaultOption)
      }
    },
    resetPreselectedOptionValue () {
      this.preselectedOptionValue = null
      this.successiveFetches = 0
    },
    onSearchTimed (value) {
      clearTimeout(this.searchTimer)
      this.searchTimer = setTimeout(() => {
        this.onSearch(value)
      }, 500)
    },
    onSearch (value) {
      this.searchQuery = value
      this.page = 1
      this.totalCount = null
      this.options = []
      if (!this.searchQuery) {
        this.addDefaultOptionIfNeeded()
      }
      this.fetchItems()
    },
    onScroll (e) {
      const nearBottom = e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 10
      const hasMore = this.options.length < this.totalCount
      if (nearBottom && hasMore && !this.loading) {
        this.fetchItems()
      }
    },
    onChange (value) {
      this.resetPreselectedOptionValue()
      this.$emit('change-option-value', value)
      if (Array.isArray(value)) {
        return
      }
      if (value === undefined || value == null) {
        this.$emit('change-option', undefined)
        return
      }
      const match = this.options.find(entry => entry[this.optionValueKey] === value)
      if (match) {
        this.$emit('change-option', match)
      }
    }
  }
}
</script>

<style lang="less" scoped>
  .select-list-footer {
    margin: 4px 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
</style>
